﻿using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using Mono.Cecil;
using WeavingCommon;

namespace NotifyPropertyWeaverMsBuildTask
{
    [Export, PartCreationPolicy(CreationPolicy.Shared)]
    public class MethodFinder
    {
        MethodInjector methodInjector;
        TypeNodeBuilder typeNodeBuilder;
        WeavingTask weavingTask;
        ModuleReader moduleReader;
        Logger logger;
        TypeResolver typeResolver;
        string eventInvokerName = "OnPropertyChanged";
        Dictionary<string, string> supportedFrameworksMapping; 

        [ImportingConstructor]
        public MethodFinder(MethodInjector methodInjector, TypeNodeBuilder typeNodeBuilder, WeavingTask weavingTask, ModuleReader moduleReader, Logger logger,TypeResolver typeResolver)
        {
            this.methodInjector = methodInjector;
            this.typeNodeBuilder = typeNodeBuilder;
            this.weavingTask = weavingTask;
            this.moduleReader = moduleReader;
            this.logger = logger;
            this.typeResolver = typeResolver;

            supportedFrameworksMapping = new Dictionary<string, string>
                                             {
                                                 {"Caliburn.Micro.PropertyChangedBase", "NotifyOfPropertyChange"},
                                                 {"Caliburn.PresentationFramework.PropertyChangedBase", "NotifyOfPropertyChange"},
                                                 {"GalaSoft.MvvmLight.ObservableObject", "RaisePropertyChanged"},
                                                 {"GalaSoft.MvvmLight.ViewModelBase", "RaisePropertyChanged"},
                                                 {"Microsoft.Practices.Prism.ViewModel.NotificationObject", "RaisePropertyChanged"},
                                                 {"Cinch.ViewModelBase", "NotifyPropertyChanged"},
                                                 //Cant support nrount because it does not have a string based notify
                                                 //{"nRoute.ViewModels.ViewModelBase", "NotifyPropertyChanged"},
                                             };
        }


        void ProcessChildNode(TypeNode node, EventInvokerMethod eventInvoker)
        {
            var childEventInvoker = FindEventInvokerMethod(node.TypeDefinition);
            if (childEventInvoker != null)
            {
                eventInvoker = childEventInvoker;
            }
            else
            {
                if (node.TypeDefinition.BaseType.IsGenericInstance)
                {
                    var methodReference = MakeGeneric(node.TypeDefinition.BaseType, eventInvoker.MethodReference);
                    eventInvoker = new EventInvokerMethod
                                       {
                                           IsBeforeAfter = eventInvoker.IsBeforeAfter,
                                           MethodReference = methodReference,
                                           IsVirtual = eventInvoker.IsVirtual
                                       };
                }
            }

            node.EventInvoker = eventInvoker;

            foreach (var childNode in node.Nodes)
            {
                ProcessChildNode(childNode, eventInvoker);
            }
        }

        public static MethodReference MakeGeneric(TypeReference declaringType, MethodReference self)
        {
            var reference = new MethodReference(self.Name, self.ReturnType)
            {
                DeclaringType = declaringType,
                HasThis = self.HasThis,
                ExplicitThis = self.ExplicitThis,
                CallingConvention = self.CallingConvention,
            };

            foreach (var parameter in self.Parameters)
            {
                reference.Parameters.Add(new ParameterDefinition(parameter.ParameterType));
            }

            return reference;
        }


        public EventInvokerMethod RecursiveFindMethod(TypeDefinition typeDefinition)
        {
            var typeDefinitions = new Stack<TypeDefinition>();
            MethodDefinition methodDefinition;
            var currentTypeDefinition = typeDefinition;
            do
            {
                typeDefinitions.Push(currentTypeDefinition);
                string methodName;
                if (supportedFrameworksMapping.TryGetValue(currentTypeDefinition.FullName, out methodName))
                {
                    var frameworkMethodDefinition = currentTypeDefinition.Methods.First(x => x.Name == methodName && IsSingleStringMethod(x));
                    if (frameworkMethodDefinition == null)
                    {
                        var message = string.Format("Found base class '{0}' and expected to find method named '{1}' but did not. It is possible the library you are using for a base class has changed the name of its event invoker. If procesing failed Please raise a bug here http://code.google.com/p/notifypropertyweaver/issues/list.", currentTypeDefinition.FullName, methodName);
                        logger.LogMessage(message);
                    }
                    else
                    {
                        methodDefinition = frameworkMethodDefinition;
                        break;
                    }
                }
                var findEventInvokerMethod = FindEventInvokerMethodDefinition(currentTypeDefinition);
                if (findEventInvokerMethod != null)
                {
                    methodDefinition = findEventInvokerMethod;
                    break;
                }
                if (currentTypeDefinition.BaseType == null || currentTypeDefinition.BaseType.FullName == "System.Object")
                {
                    return null;
                }
                currentTypeDefinition = typeResolver.Resolve(currentTypeDefinition.BaseType);
            } while (true);

            var methodReference = moduleReader.Module.Import(methodDefinition).GetGeneric();
            typeDefinitions.Pop();
            while (typeDefinitions.Count > 0)
            {
                var definition = typeDefinitions.Pop();
                methodReference = MakeGeneric(definition.BaseType, methodReference);
            }
            return new EventInvokerMethod
                       {
                           MethodReference = methodReference,
                           IsBeforeAfter = IsBeforeAfterMethod(methodDefinition),
                           IsVirtual = methodDefinition.IsVirtual
                       };
        }


        EventInvokerMethod FindEventInvokerMethod(TypeDefinition type)
        {
            var methodDefinition = FindEventInvokerMethodDefinition(type);
            if (methodDefinition != null)
            {
                var methodReference = moduleReader.Module.Import(methodDefinition);
                return new EventInvokerMethod
                           {
                               MethodReference = methodReference.GetGeneric(),
                               IsBeforeAfter = IsBeforeAfterMethod(methodDefinition),
                               IsVirtual = methodDefinition.IsVirtual,
                           };
            }
            return null;
        }
        MethodDefinition FindEventInvokerMethodDefinition(TypeDefinition type)
        {
            return type.Methods
                .Where(x => x.Name == eventInvokerName)
                .OrderByDescending(definition => definition.Parameters.Count)
                .FirstOrDefault(x => IsBeforeAfterMethod(x) || IsSingleStringMethod(x));
        }

        public bool IsSingleStringMethod(MethodDefinition method)
        {
            var parameters = method.Parameters;
            return parameters.Count == 1
                   && parameters[0].ParameterType.FullName == "System.String";
        }

        public bool IsBeforeAfterMethod(MethodDefinition method)
        {
            var parameters = method.Parameters;
            return parameters.Count == 3
                   && parameters[0].ParameterType.FullName == "System.String"
                   && parameters[1].ParameterType.FullName == "System.Object"
                   && parameters[2].ParameterType.FullName == "System.Object";
        }

    
        public void Execute()
        {
            eventInvokerName = weavingTask.EventInvokerName;
            foreach (var notifyNode in typeNodeBuilder.NotifyNodes)
            {
                var eventInvoker = RecursiveFindMethod(notifyNode.TypeDefinition);
                if (eventInvoker == null)
                {
                    eventInvoker = methodInjector.AddOnPropertyChagedMethod(notifyNode.TypeDefinition);
                    if (eventInvoker == null)
                    {
                        logger.LogMessage(string.Format("\tCould not derive or inject '{0}' into '{1}'. It is possible you inheriting from a base class and have not correctly set 'WeavingTask.EventInvokerName'.", weavingTask.EventInvokerName, notifyNode.TypeDefinition.Name));
                    }
                }
               
                notifyNode.EventInvoker = eventInvoker;

                foreach (var childNode in notifyNode.Nodes)
                {
                    ProcessChildNode(childNode, eventInvoker);
                }
            }
        }

    }
}